﻿using System;
using System.Linq;
using System.Collections.Generic;
using XNOgre.Core;
using XNOgre.Graphics;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace XNOgre.Scene
{
    public struct FogSettings : IEffectFog
    {
        public Vector3 FogColor
        {
            get;
            set;
        }

        public bool FogEnabled
        {
            get;
            set;
        }

        public float FogEnd
        {
            get;
            set;
        }

        public float FogStart
        {
            get;
            set;
        }
    }

    public class SceneManager : IDisposable
    {
        #region NestedTypes
        private class SkyBoxParams
        {
            public bool Enable { get; set; }
            public RenderQueueGroupID RenderQueue { get; set; }
            public Quaternion Orientation { get; set; }
            public string MaterialName { get; set; }
            public float Distance { get; set; }

            public SkyBoxParams()
            {
                RenderQueue = RenderQueueGroupID.SkiesEarly;
                Distance = 5000.0f;
                Orientation = Quaternion.Identity;
            }
        }
        private class SkyDomeParams
        {
            public bool Enable { get; set; }
            public string MaterialName { get; set; }
            public float Tiling { get; set; }
            public float Distance { get; set; }
            public RenderQueueGroupID RenderQueue { get; set; }
            public Quaternion Orientation { get; set; }

            public SkyDomeParams()
            {
                Tiling = 8;
                Distance = 4000;
                RenderQueue = RenderQueueGroupID.SkiesEarly;
            }
        }
        private class SkyPlaneParams
        {
            public bool Enable { get; set; }
            public Plane Plane { get; set; }
            public RenderQueueGroupID RenderQueue { get; set; }
            public string MaterialName { get; set; }
            public float Scale { get; set; }
            public float Tiling { get; set; }

            public SkyPlaneParams()
            {
                Scale = 1000.0f;
                RenderQueue = RenderQueueGroupID.SkiesEarly;
                Tiling = 10.0f;
            }
        }
        #endregion

        #region Events/EventArgs
        public class UpdateSceneGraphArgs : EventArgs
        {
            public UpdateSceneGraphArgs(SceneManager source, Camera cam)
            {
                this.Source = source;
                this.Camera = cam;
            }
            public SceneManager Source {get; private set;}
            public Camera Camera {get; private set;}
        }

        public class FindVisibleObjectEventArgs : EventArgs
        {
            public FindVisibleObjectEventArgs(SceneManager source, ViewportTarget viewport)
            {
                this.Source = source;
                this.Viewport = viewport;
            }
            public SceneManager Source { get; private set; }
            public ViewportTarget Viewport { get; private set; }
        }
        public event EventHandler<UpdateSceneGraphArgs> PreUpdateSceneGraph;
        public event EventHandler<UpdateSceneGraphArgs> PostUpdateSceneGraph;

        public event EventHandler<FindVisibleObjectEventArgs> PreFindVisibleObjects;
        public event EventHandler<FindVisibleObjectEventArgs> PostFindVisibleObjects;

        public event EventHandler SceneMangerDestroyed;
        #endregion

        #region Fields
        private string name;
        private ViewportTarget currentViewport;
        private Camera currentCamera;

        //shadows
        private bool shadowsEnabled;

        //scene objects
        private List<Camera> cameras;
        private List<SceneNode> sceneNodes;
        private List<MovableObject> movableObjects;
        private SceneNode sceneRoot;

        //sky
        private Entity skyPlane;
        private Entity skyDome;
        private Entity[] skyBox = new Entity[6];
        
        private SkyPlaneParams skyPlaneParams;
        private SkyDomeParams skyDomeParams;
        private SkyBoxParams skyBoxParams;
        private SceneNode skyNode;
        #endregion

        #region C'tor/D'tor
        public SceneManager(string instanceName)
        {
            this.name = instanceName;

            cameras = new List<Camera>();
            sceneNodes = new List<SceneNode>();
            movableObjects = new List<MovableObject>();
            shadowsEnabled = false;
            Fog = new FogSettings();
            ShadowColor = Color.Black;

        }
     
        public void Dispose()
        {
            if (SceneMangerDestroyed != null)
            {
                SceneMangerDestroyed(this, EventArgs.Empty);
            }
            ClearScene();
            DestroyAllCameras();

            lock (movableObjects)
            {
                for (int i = 0; i < movableObjects.Count; i++)
                {
                    movableObjects[i].Dispose();
                    movableObjects[i] = null;
                }
                movableObjects.Clear();
            }

            for (int i = 0; i < skyBox.Length; i++)
            {
                if (skyBox[i] != null)
                {
                    skyBox[i].Dispose();
                    skyBox[i] = null;
                }
            }
            skyBox = null;

            sceneRoot.Dispose();
            sceneRoot = null;
        }
        #endregion

        #region Scene Properties
        public string Name
        {
            get { return name; }
            set { name = value; }
        }
        public virtual SceneNode RootSceneNode
        {
            get
            {
                if (sceneRoot == null)
                {
                    sceneRoot = CreateSceneNode("XNOgre/SceneRoot");
                    sceneRoot.NotifyRootNode();
                }
                return sceneRoot;
            }
        }
        public virtual Color AmbientLight
        {
            get
            {
                return Root.Instance.RenderSystem.AmbientLight;
            }
            set
            {
                Root.Instance.RenderSystem.AmbientLight = value;
            }
        }
        public FogSettings Fog
        {
            get
            {
                return (FogSettings)Root.Instance.RenderSystem.Fog;
            }
            set
            {
                Root.Instance.RenderSystem.Fog = value;
            }
        }

        public bool ShadowsEnabled
        {
            get { return shadowsEnabled; }
            set
            { 
                shadowsEnabled = value;
                if (value)
                {
                    throw new NotImplementedException("Shadows have yet to be implemented");
                }
            }
        }
        public Color ShadowColor { get; set; }

        #endregion

        #region Scene Methods
        public virtual SceneNode CreateSceneNode()
        {
            SceneNode sn = new SceneNode(this);
            sceneNodes.Add(sn);
            return sn;
        }
        public virtual SceneNode CreateSceneNode(string name)
        {
            for (int i = 0; i < sceneNodes.Count; i++)
            {
                if (sceneNodes[i].Name == name)
                {
                    throw new XNOgreException("A SceneNode with the name " + name + " already exists.");
                }
            }

            SceneNode sn = new SceneNode(this, name);
            sceneNodes.Add(sn);
            return sn;
        }
        public virtual void DestroySceneNode(string name)
        {
            for (int i = 0; i < sceneNodes.Count; i++)
            {
                if (sceneNodes[i].Name == name)
                {
                    sceneNodes[i].Dispose();
                    sceneNodes[i] = null;
                    sceneNodes.RemoveAt(i);
                    break;
                }
            }
        }
        public virtual void DestroySceneNode(SceneNode sn)
        {
            DestroySceneNode(sn.Name);
        }
        public virtual SceneNode GetSceneNode(string name)
        {
            for (int i = 0; i < sceneNodes.Count; i++)
            {
                if (sceneNodes[i].Name == name)
                    return sceneNodes[i];
            }

            throw new XNOgreException("SceneNode " + name + " not found.");
        }
        public virtual bool HasSceneNode(string name)
        {
            for (int i = 0; i < sceneNodes.Count; i++)
            {
                if (sceneNodes[i].Name == name)
                    return true;
            }

            return false;
        }
        
        public virtual Entity CreateEntity(string entName, string meshName)
        {
            for (int i = 0; i < movableObjects.Count; i++)
            {
                if (movableObjects[i] is Entity && movableObjects[i].Name == entName)
                {
                    throw new XNOgreException("An entity named " + entName + " already exists.");
                }
            }
            Entity ent = new Entity(entName, meshName);
            movableObjects.Add(ent);
            return ent;
        }
        public virtual Entity CreateEntity(string meshName)
        {
            Entity ent = new Entity(meshName);
            movableObjects.Add(ent);
            return ent;
        }
        public virtual Entity CreateEntity(string name, RenderOperation mesh)
        {
            for (int i = 0; i < movableObjects.Count; i++)
            {
                if (movableObjects[i] is Entity && movableObjects[i].Name == name)
                {
                    throw new XNOgreException("An entity named " + name + " already exists.");
                }
            }
            Entity ent = new Entity(name, mesh);
            movableObjects.Add(ent);
            return ent;
        }
        public virtual Entity GetEntity(string name)
        {
            for (int i = 0; i < movableObjects.Count; i++)
            {
                if (movableObjects[i] is Entity && movableObjects[i].Name == name)
                {
                    return movableObjects[i] as Entity;
                }
            }

            throw new XNOgreException("Entity " + name + " not found.");
        }
        public virtual bool HasEntity(string name)
        {
            for (int i = 0; i < movableObjects.Count; i++)
            {
                if (movableObjects[i] is Entity && movableObjects[i].Name == name)
                    return true;
            }
            return false;
        }
        public virtual void DestroyEntity(Entity ent)
        {
            DestroyEntity(ent.Name);
        }
        public virtual void DestroyEntity(string name)
        {
            for (int i = 0; i < movableObjects.Count; i++)
            {
                if (movableObjects[i] is Entity && movableObjects[i].Name == name)
                {
                    
                    movableObjects[i].Dispose();
                    movableObjects[i] = null;
                    movableObjects.RemoveAt(i);
                    break;
                }
            }
        }
        public virtual void DestroyAllEntities()
        {
            for (int i = 0; i < movableObjects.Count; i++)
            {
                if (movableObjects[i] is Entity)
                {
                    movableObjects[i].Dispose();
                    movableObjects[i] = null;
                    movableObjects.RemoveAt(i);
                }
            }

        }
        public virtual BillboardSet CreateBillboardSet(int poolSize = 20)
        {
            return CreateBillboardSet(Root.GenerateName(typeof(BillboardSet)), poolSize);
        }
        public virtual BillboardSet CreateBillboardSet(string name, int poolSize = 20)
        {
            BillboardSet set = new BillboardSet(name, poolSize);
            movableObjects.Add(set);
            return set;
        }
        public virtual BillboardSet GetBillboardSet(string name)
        {
            for (int i = 0; i < movableObjects.Count; i++)
            {
                if (movableObjects[i] is BillboardSet && movableObjects[i].Name == name)
                    return (movableObjects[i] as BillboardSet);
            }

            throw new XNOgreException("A BillboardSet named " + name + " was not found.");
        }
        public virtual bool HasBillboardSet(string name)
        {
            for (int i = 0; i < movableObjects.Count; i++)
            {
                if (movableObjects[i] is BillboardSet && movableObjects[i].Name == name)
                    return true;
            }
            return false;
        }
        public virtual void DestroyBillboardSet(BillboardSet set)
        {
            for (int i = 0; i < movableObjects.Count; i++)
            {
                if (movableObjects[i] == set)
                {
                    movableObjects[i].Dispose();
                    movableObjects[i] = null;
                    movableObjects.RemoveAt(i);
                    break;
                }
            }
        }
        public virtual void DestroyBillboardSet(string name)
        {
            DestroyBillboardSet(GetBillboardSet(name));
        }
        public virtual void DestroyAllBillboardSets()
        {
            for (int i = 0; i < movableObjects.Count; i++)
            {
                if (movableObjects[i] is BillboardSet)
                {
                    movableObjects[i].Dispose();
                    movableObjects[i] = null;
                    movableObjects.RemoveAt(i);
                }
            }
        }
        public virtual Camera CreateCamera(string name)
        {
            foreach (var camera in cameras)
            {
                if (camera.Name == name)
                {
                    throw new XNOgreException("A camera with the name " + name + " exists");
                }
            }

            Camera cam = new Camera(name);
            cam.SceneManager = this;
            cameras.Add(cam);
            return cam;
        }
        public virtual Camera GetCamera(string name)
        {
            foreach (var cam in cameras)
            {
                if (cam.Name == name)
                    return cam;
            }

            throw new XNOgreException("Cannot find Camera with name " + name);
        }
        public virtual bool HasCamera(string name)
        {
            foreach (var cam in cameras)
            {
                if (cam.Name == name)
                    return true;
            }
            return false;
        }
        public virtual void DestroyCamera(Camera cam)
        {
            DestroyCamera(cam.Name);
        }
        public virtual void DestroyCamera(string name)
        {
            for (int i = 0; i < cameras.Count; i++)
            {
                if (cameras[i].Name == name)
                {
                    cameras[i].Dispose();
                    cameras[i] = null;
                    cameras.RemoveAt(i);
                    break;
                }
            }
        }
        public virtual void DestroyAllCameras()
        {
            for (int i = 0; i < cameras.Count; i++)
            {
                cameras[i].Dispose();
                cameras[i] = null;
            }
            cameras.Clear();
        }

        public virtual Light CreateLight(string name)
        {
            Light l = new Light(name);
            movableObjects.Add(l);
            return l;
        }
        public virtual Light CreateLight()
        {
            Light l = new Light();
            movableObjects.Add(l);
            return l;
        }
        public virtual Light GetLight(string name)
        {
            for (int i = 0; i < movableObjects.Count; i++)
            {
                if (movableObjects[i] is Light && movableObjects[i].Name == name)
                {
                    return movableObjects[i] as Light;
                }
            }

            throw new XNOgreException("Light with name " + name + " not found.");
        }
        public virtual bool HasLight(string name)
        {
            for (int i = 0; i < movableObjects.Count; i++)
            {
                if (movableObjects[i] is Light && movableObjects[i].Name == name)
                {
                    return true;
                }
            }
            return false;
        }
        public virtual void DestroyLight(string name)
        {
            for (int i = 0; i < movableObjects.Count; i++)
            {
                if (movableObjects[i] is Light && movableObjects[i].Name == name)
                {
                    movableObjects[i].Dispose();
                    movableObjects[i] = null;
                    movableObjects.RemoveAt(i);
                    break;
                }
            }
        }
        public virtual void DestroyLight(Light light)
        {
            DestroyLight(light.Name);
        }
        public virtual void DestroyAllLights()
        {
            for (int i = 0; i < movableObjects.Count; i++)
            {
                if (movableObjects[i] is Light)
                {
                    movableObjects[i].Dispose();
                    movableObjects[i] = null;
                    movableObjects.RemoveAt(i);
                }
            }
        }
        public List<Tuple<MovableObject, float>> ExecuteRaySceneQuery(Ray ray, bool sortByDistance = true)
        {
            List<Tuple<MovableObject, float>> results = new List<Tuple<MovableObject,float>>();

            for (int i = 0; i < movableObjects.Count; i++)
            {
                float? entry = ray.Intersects(movableObjects[i].BoundingBox);
                if (entry.HasValue && entry.Value != 0)
                {
                    results.Add(new Tuple<MovableObject, float>(movableObjects[i], entry.Value));
                }
            }

            if (sortByDistance)
            {
                results.Sort(CompareRayResult);
            }
            return results;
        }
        private static int CompareRayResult(Tuple<MovableObject, float> x, Tuple<MovableObject, float> y)
        {
            return x.Item2.CompareTo(y.Item2);
        }

        public virtual void SetSkyPlane(bool enable, Plane plane, string materialName, float scale = 1000.0f, float tiling = 10.0f, RenderQueueGroupID renderQueue = RenderQueueGroupID.SkiesEarly)
        { }
        public virtual void SetSkyBox(bool enable, Material material, Quaternion orientation, float distance = 5000.0f, RenderQueueGroupID renderQueue = RenderQueueGroupID.SkiesEarly)
        {
            if (enable)
            {
                BillboardSet skySides = new BillboardSet("XNOgre/Skybox");
                skySides.Material = material;

                Vector3 up = new Vector3(0, distance, 0);
                Vector3 down = new Vector3(0, -distance, 0);
                Vector3 forward = new Vector3(0, 0, -distance);
                Vector3 backward = new Vector3(0, 0, distance);
                Vector3 left = new Vector3(-distance, 0, 0);
                Vector3 right = new Vector3(distance, 0, 0);

              //  skySides.CreateBillboard(
            }
        }

        public virtual void SetSkyDome(bool enable, string materialName, Quaternion orientation, float tiling = 8, float distance = 5000.0f, RenderQueueGroupID renderQueue = RenderQueueGroupID.SkiesEarly)
        { }

        public void ClearScene()
        {
            for (int i = 0; i < movableObjects.Count; i++)
            {
                movableObjects[i].Dispose();
                movableObjects[i] = null;
            }
            movableObjects.Clear();

            RootSceneNode.RemoveAllChildren();
            RootSceneNode.DetachAllObjects();

            for (int i = 0; i < sceneNodes.Count; i++)
            {
                sceneNodes[i].Dispose();
                sceneNodes[i] = null;
            }
            sceneNodes.Clear();
        }
        #endregion

        #region Rendering Methods
        protected virtual IEnumerable<Light> GetSceneLights()
        {
            for (int i = 0; i < movableObjects.Count; i++)
            {
                if (movableObjects[i] is Light)
                    yield return movableObjects[i] as Light;
            }
        }
        private bool RenderGroupShouldDoShadows(RenderQueueGroupID renderGroup)
        {
            if (renderGroup == RenderQueueGroupID.Background || renderGroup == RenderQueueGroupID.Overlay || renderGroup == RenderQueueGroupID.SkiesEarly || renderGroup == RenderQueueGroupID.SkiesLate)
                return false;

            return true;
        }

        private void UpdateSkyBox(Camera camera)
        {
            if (skyNode != null)
            {
                skyNode.Position = camera.DerivedPosition;
            }
        }
        private void RenderSingleObject(IRenderable rend, Pass pass)
        {
            Root.Instance.RenderSystem.World = rend.WorldTransforms;
            Root.Instance.RenderSystem.SetLights(rend.Lights);
            Root.Instance.RenderSystem.SetPassSettings(pass);

            Root.Instance.RenderSystem.Render(rend.RenderOperation);
        }
        private IEnumerable<IRenderable> FindShadowCastersForLight(Light light)
        {
            List<IRenderable> shadowCasters = new List<IRenderable>();

            for (int i = 0; i < movableObjects.Count; i++)
            {
                if (movableObjects[i].CastsShadows == false || movableObjects[i].ParentNode == null)
                    continue;

                if (light.Type == Light.LightType.Directional)
                {
                    movableObjects[i].UpdateRenderQueue(ref shadowCasters);
                }
                else
                {
                    BoundingSphere sphere = new BoundingSphere(light.Position, light.AttenuationRange);
                    if (sphere.Contains(movableObjects[i].ParentNode.DerivedPosition) != ContainmentType.Disjoint)
                    {
                        movableObjects[i].UpdateRenderQueue(ref shadowCasters);
                    }
                }
            }

            return shadowCasters;
        }


        private void RenderBasicQueueGroupObjects(IEnumerable<IRenderable> groupObjects)
        {
            var priorityGroup = from item in groupObjects orderby item.GetSquaredViewDepth(currentCamera) descending select item;
            //solids first
            foreach (var rend in priorityGroup)
            {
                rend.Lights = GetSceneLights();
                foreach (var pass in rend.Material.CurrentTechnique.Passes)
                {
                    if (pass.Alpha == 1)
                    {
                        RenderSingleObject(rend, pass);
                    }
                }
            }

            //transparents now
            foreach (var rend in priorityGroup)
            {
                rend.Lights = GetSceneLights();
                foreach (var pass in rend.Material.CurrentTechnique.Passes)
                {
                    if (pass.Alpha != 1)
                    {
                        RenderSingleObject(rend, pass);
                    }
                }
            }
        }

        private void RenderQueueGroupObjects(RenderQueueGroupID renderGroup, IEnumerable<IRenderable> groupObjects)
        {
            bool doShadows = RenderGroupShouldDoShadows(renderGroup) && currentViewport.ShadowsEnabled && shadowsEnabled;
            if (doShadows)
            {
                 //RenderModulativeTextureShadowedQueueGroupObjects(groupObjects);
            }
            else //totally ordinary pass
            {
                RenderBasicQueueGroupObjects(groupObjects);
            }
        }
        private void RenderVisibleObjects(IEnumerable<IRenderable> visibleObjects)
        {
            Dictionary<RenderQueueGroupID, List<IRenderable>> queueGroups = new Dictionary<RenderQueueGroupID, List<IRenderable>>();
            foreach (var renderable in visibleObjects)
            {
                if (queueGroups.ContainsKey(renderable.RenderQueueGroup))
                {
                    queueGroups[renderable.RenderQueueGroup].Add(renderable);
                }
                else
                {
                    List<IRenderable> currentGroup = new List<IRenderable>();
                    currentGroup.Add(renderable);
                    queueGroups.Add(renderable.RenderQueueGroup, currentGroup);
                }
            }

            var sortedQueues = from queueGroup in queueGroups orderby (int)queueGroup.Key ascending select queueGroup;
            foreach (var group in sortedQueues)
            {
                RenderQueueGroupObjects(group.Key, group.Value);
            }
        }

        internal void RenderScene(Camera camera, ViewportTarget vp, bool includeOverlays)
        {
            currentViewport = vp;
            currentCamera = camera;

            Root.Instance.RenderSystem.SetViewportSettings(vp);
            Root.Instance.RenderSystem.SetCameraSettings(camera);

            IEnumerable<IRenderable> visibleObjects = null;

            lock (this)
            {
                UpdateSceneGraph(camera);

                if (PreFindVisibleObjects != null)
                {
                    PreFindVisibleObjects(this, new FindVisibleObjectEventArgs(this, vp));
                }

                visibleObjects = FindVisibleObjects(camera);

                if (PostFindVisibleObjects != null)
                {
                    PostFindVisibleObjects(this, new FindVisibleObjectEventArgs(this, vp));
                }

                //NB: replaces ogres "_queueSkiesForRendering method.
                UpdateSkyBox(camera);
            } //end lock
            
            Root.Instance.RenderSystem.Projection = camera.Proj;
            Root.Instance.RenderSystem.View = camera.View;

            RenderVisibleObjects(visibleObjects);
        }
        
        private IEnumerable<IRenderable> FindVisibleObjects(Camera cam)
        {
            return RootSceneNode.FindVisibleObjects(cam, true);
        }
        private void UpdateSceneGraph(Camera cam)
        {
            if (PreUpdateSceneGraph != null)
            {
                PreUpdateSceneGraph(this, new UpdateSceneGraphArgs(this, cam));
            }
            RootSceneNode.Update(true, false);
            if (PostUpdateSceneGraph != null)
            {
                PostUpdateSceneGraph(this, new UpdateSceneGraphArgs(this, cam));
            }
        }
        #endregion

    }
}